知识篇 -- JS Web Components详解

Ray Shine 2024/2/20 JavaScript进阶知识Web Components组件化

在现代前端开发中,组件化是提高开发效率和代码可维护性的核心思想。虽然React、Vue等框架提供了强大的组件化能力,但Web Components作为浏览器原生的组件化技术,提供了一种无需依赖任何框架即可构建可复用、封装性强的组件的标准。Web Components由三项主要技术组成:Custom ElementsShadow DOMHTML Templates

# 一、Custom Elements (自定义元素) 自定义标签

Custom Elements允许开发者定义自己的HTML标签。这些自定义标签拥有自己的行为和生命周期,可以像原生HTML标签一样使用。

  • 定义:通过 customElements.define() 方法注册自定义元素。
    • name:自定义元素的标签名,必须包含连字符(例如 my-button, user-card),以避免与现有或未来的HTML标签冲突。
    • constructor:自定义元素的JavaScript类,必须继承自 HTMLElement
  • 生命周期回调:自定义元素提供了一系列生命周期回调函数,允许开发者在元素的不同阶段执行代码。
    • connectedCallback():当元素首次被插入到文档DOM时调用。
    • disconnectedCallback():当元素从文档DOM中移除时调用。
    • adoptedCallback():当元素被移动到新的文档时调用。
    • attributeChangedCallback(name, oldValue, newValue):当元素的属性被添加、移除或更改时调用。需要配合 static get observedAttributes() 指定要观察的属性。

示例

class MyCustomButton extends HTMLElement {
    constructor() {
        super(); // 必须调用super()
        this.addEventListener('click', this.handleClick);
    }

    connectedCallback() {
        if (!this.rendered) {
            this.innerHTML = `<button>${this.getAttribute('text') || 'Default Button'}</button>`;
            this.rendered = true;
        }
        console.log('MyCustomButton connected to DOM');
    }

    disconnectedCallback() {
        this.removeEventListener('click', this.handleClick);
        console.log('MyCustomButton disconnected from DOM');
    }

    handleClick() {
        alert(`Button "${this.getAttribute('text') || 'Default Button'}" clicked!`);
    }

    static get observedAttributes() {
        return ['text']; // 声明要观察的属性
    }

    attributeChangedCallback(name, oldValue, newValue) {
        if (name === 'text' && oldValue !== newValue) {
            this.querySelector('button').textContent = newValue;
        }
    }
}

customElements.define('my-custom-button', MyCustomButton);

使用

<my-custom-button text="Click Me"></my-custom-button>
<my-custom-button></my-custom-button>
<script>
    const btn = document.querySelector('my-custom-button');
    setTimeout(() => {
        btn.setAttribute('text', 'New Text'); // 触发attributeChangedCallback
    }, 2000);
</script>

# 二、Shadow DOM (影子DOM) 样式隔离

Shadow DOM提供了一种将DOM和CSS封装起来的方式,使得组件的内部结构和样式不会泄露到外部,也不会受到外部样式的影响。

  • 作用:实现组件的样式和结构隔离。
  • attachShadow() 方法:用于将一个Shadow DOM树附加到指定的元素上。
    • mode: 'open':Shadow DOM可以通过JavaScript从外部访问(例如 element.shadowRoot)。
    • mode: 'closed':Shadow DOM无法从外部访问。
  • 特点
    • 样式隔离:Shadow DOM内部的CSS不会影响外部,外部的CSS也不会影响内部(除非使用CSS变量或特定的穿透选择器)。
    • DOM隔离:Shadow DOM内部的DOM结构不会被外部的 querySelector 等方法选中。
  • 示例
    class UserCard extends HTMLElement {
        constructor() {
            super();
            // 附加Shadow DOM
            this.attachShadow({ mode: 'open' });
    
            // 创建内部DOM结构
            this.shadowRoot.innerHTML = `
                <style>
                    .card {
                        border: 1px solid #ccc;
                        padding: 10px;
                        border-radius: 5px;
                        display: flex;
                        align-items: center;
                        gap: 10px;
                        background-color: #f9f9f9;
                    }
                    .avatar {
                        width: 50px;
                        height: 50px;
                        border-radius: 50%;
                        background-color: #ddd;
                        display: flex;
                        justify-content: center;
                        align-items: center;
                        font-weight: bold;
                    }
                    .name {
                        font-weight: bold;
                        color: blue; /* 这个样式只在Shadow DOM内部生效 */
                    }
                </style>
                <div class="card">
                    <div class="avatar"><slot name="avatar-initial"></slot></div>
                    <div class="name"><slot name="user-name"></slot></div>
                    <div class="email"><slot name="user-email"></slot></div>
                </div>
            `;
        }
    }
    
    customElements.define('user-card', UserCard);
    

使用

<user-card>
    <span slot="avatar-initial">JS</span>
    <span slot="user-name">John Doe</span>
    <span slot="user-email">john.doe@example.com</span>
</user-card>

<style>
    /* 这个样式不会影响user-card内部的.name元素 */
    .name {
        color: red;
    }
</style>

解释slot 元素是Shadow DOM中的占位符,允许外部内容插入到Shadow DOM的特定位置。

# 三、HTML Templates (<template><slot>) 可复用结构

HTML Templates (<template> 标签) 允许您定义可重用的HTML结构,这些结构在页面加载时不会被渲染,但可以通过JavaScript进行实例化和使用。<slot> 标签则用于在Shadow DOM中创建内容插入点。

  • <template> 标签
    • 作用:用于声明一个HTML片段,该片段在页面加载时不会被渲染,但可以通过JavaScript访问其内容 (template.content)。
    • 特点:内容是惰性的,不会影响DOM,也不会加载图片或执行脚本。
  • <slot> 标签
    • 作用:作为Shadow DOM中的占位符,允许外部内容(Light DOM)插入到Shadow DOM的特定位置。
    • 具名插槽:通过 name 属性可以创建多个具名插槽,外部内容通过 slot 属性指定插入到哪个插槽。

示例 (结合Custom Elements和Shadow DOM):

const template = document.createElement('template');
template.innerHTML = `
    <style>
        .tooltip-container {
            position: relative;
            display: inline-block;
            border-bottom: 1px dotted black;
        }
        .tooltip-text {
            visibility: hidden;
            width: 120px;
            background-color: #555;
            color: #fff;
            text-align: center;
            border-radius: 6px;
            padding: 5px 0;
            position: absolute;
            z-index: 1;
            bottom: 125%;
            left: 50%;
            margin-left: -60px;
            opacity: 0;
            transition: opacity 0.3s;
        }
        .tooltip-container:hover .tooltip-text {
            visibility: visible;
            opacity: 1;
        }
    </style>
    <div class="tooltip-container">
        <slot name="content"></slot>
        <span class="tooltip-text"><slot name="message"></slot></span>
    </div>
`;

class MyTooltip extends HTMLElement {
    constructor() {
        super();
        this.attachShadow({ mode: 'open' });
        this.shadowRoot.appendChild(template.content.cloneNode(true));
    }
}

customElements.define('my-tooltip', MyTooltip);

使用

<my-tooltip>
    <span slot="content">Hover over me</span>
    <span slot="message">This is a tooltip!</span>
</my-tooltip>

# 四、Web Components的优势与局限性 优缺点

# 优势:

  • 原生组件化:无需框架即可构建组件,具有良好的互操作性。
  • 封装性:Shadow DOM提供强大的样式和DOM隔离,避免冲突。
  • 可复用性:一旦定义,可以在任何HTML页面中像原生标签一样使用。
  • 互操作性:可以与任何JavaScript框架(如React、Vue)协同工作。

# 局限性:

  • 浏览器兼容性:虽然现代浏览器支持良好,但对于旧版浏览器可能需要Polyfill。
  • SEO:Shadow DOM中的内容默认对搜索引擎不友好,需要额外处理。
  • 工具链:相对于成熟的框架生态,Web Components的工具链(如状态管理、路由)相对不完善。

# 五、总结

Web Components提供了一套浏览器原生的组件化标准,使得开发者能够构建出可复用、封装性强的自定义HTML元素。通过Custom Elements定义组件行为,Shadow DOM实现样式和结构隔离,以及HTML Templates提供可复用的DOM结构,Web Components为前端组件化提供了一个强大的底层基础。虽然在某些方面仍有待完善,但它代表了Web平台组件化的未来方向。

最后更新时间: 2025/11/20 22:59:30
ON THIS PAGE